package org.xx.armory.commons;

import org.apache.commons.lang3.time.DateUtils;

import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import static org.xx.armory.commons.Validators.notNull;

/**
 * 日期时间相关的工具类。
 *
 * @author Dev
 */
public final class DateTimeUtils {
    /**
     *
     */
    private static final String GMT_TIMEZONE_ID = "GMT";

    private static final Date MIN_VALUE = new Date(0L);

    /**
     * 3000/01/03 GMT
     */
    private static final Date MAX_VALUE = new Date(32503852800000L);
    private static final long HOURS_PER_DAY = 24L;
    private static final long MINUTES_PER_HOUR = 60L;
    private static final long SECONDS_PER_MINUTE = 60L;
    private static final long MILLISECONDS_PER_SECOND = 1000L;
    private static final long MILLISECONDS_PER_DAY = MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY;

    /**
     * 禁止构造类型。
     */
    private DateTimeUtils() {
        throw new UnsupportedOperationException();
    }

    /**
     * 获取最小可能日期，目前该值是1970/01/01 GMT。
     *
     * @return 最小可能日期。
     */
    public static Date minValue() {
        return MIN_VALUE;
    }

    /**
     * 获取最大可能日期，目前该值是3000/01/01 GMT。
     *
     * @return 最大可能日期。
     */
    public static Date maxValue() {
        return MAX_VALUE;
    }

    /**
     * 创建本地日期对象。
     *
     * <p>
     * 该日期对象的时分秒和毫秒都是0。
     * </p>
     *
     * @param year
     *         年份
     * @param month
     *         月份(从0开始)
     * @param day
     *         每月的日期
     * @return 按照指定参数构造的日期对象。
     * @see #createLocalDate(int, int, int, int, int, int, int)
     */
    public static Calendar createLocalDate(
            final int year,
            final int month,
            final int day
    ) {
        return createLocalDate(year, month, day, 0, 0, 0);
    }

    /**
     * 创建本地日期对象。
     *
     * <p>
     * 该日期对象的毫秒都是0。
     * </p>
     *
     * @param year
     *         年份
     * @param month
     *         月份(从0开始)
     * @param day
     *         每月的日期
     * @param hour
     *         小时
     * @param minute
     *         分钟
     * @param second
     *         秒
     * @return 按照指定参数构造的日期对象。
     */
    public static Calendar createLocalDate(
            final int year,
            final int month,
            final int day,
            int hour,
            int minute,
            int second
    ) {
        Calendar c = Calendar.getInstance();
        c.setLenient(false);
        c.set(year, month, day, hour, minute, second);
        c.set(Calendar.MILLISECOND, 0);

        return c;
    }

    /**
     * 创建GMT日期对象。
     *
     * <p>
     * 该日期对象的时分秒和毫秒都是0。
     * </p>
     *
     * @param year
     *         年份
     * @param month
     *         月份(从0开始)
     * @param day
     *         每月的日期
     * @return 按照指定参数构造的日期对象。
     * @see #createGMTDate(int, int, int, int, int, int)
     */
    public static Calendar createGMTDate(
            final int year,
            final int month,
            final int day
    ) {
        return createGMTDate(year, month, day, 0, 0, 0);
    }

    /**
     * 创建GMT日期对象。
     *
     * <p>
     * 该日期对象的毫秒都是0。
     * </p>
     *
     * @param year
     *         年份
     * @param month
     *         月份(从0开始)
     * @param day
     *         每月的日期
     * @param hour
     *         小时
     * @param minute
     *         分钟
     * @param second
     *         秒
     * @return 按照指定参数构造的日期对象。
     */
    public static Calendar createGMTDate(
            final int year,
            final int month,
            final int day,
            final int hour,
            final int minute,
            final int second
    ) {
        Calendar c = Calendar.getInstance();
        c.setTimeZone(TimeZone.getTimeZone(GMT_TIMEZONE_ID));
        c.setLenient(false);
        c.set(year, month, day, hour, minute, second);
        c.set(Calendar.MILLISECOND, 0);

        return c;
    }

    /**
     * 创建本地日期对象。
     *
     * @param year
     *         年份
     * @param month
     *         月份(从0开始)
     * @param day
     *         每月的日期
     * @param hour
     *         小时
     * @param minute
     *         分钟
     * @param second
     *         秒
     * @param mills
     *         毫秒
     * @return 按照指定参数构造的日期对象。
     */
    public static Calendar createLocalDate(
            final int year,
            final int month,
            final int day,
            final int hour,
            final int minute,
            final int second,
            final int mills
    ) {
        Calendar c = Calendar.getInstance();
        c.setLenient(false);
        c.set(year, month, day, hour, minute, second);
        c.set(Calendar.MILLISECOND, mills);

        return c;
    }

    /**
     * 获取当前日期时间对象。
     *
     * <p>
     * 毫秒部分是{@code 0}。
     * </p>
     *
     * @return 当前日期时间对象。
     */
    public static Calendar now() {
        Calendar c = Calendar.getInstance();
        c.setLenient(false);
        c.set(Calendar.MILLISECOND, 0);

        return c;
    }

    /**
     * 将日期时间对象调整为本地时区。
     *
     * @param c
     *         待调整的日期时间对象, 可以是任意时区。
     * @return 调整为本地时区的日期时间对象。
     * @throws NullPointerException
     *         如果参数{@code c}是{@code null}。
     */
    public static Calendar toLocal(final Calendar c) {
        notNull(c, "c");

        Calendar rc = Calendar.getInstance();
        rc.setLenient(false);
        rc.setTime(c.getTime());

        return rc;
    }

    /**
     * 将日期时间对象调整为GMT时区。
     *
     * @param c
     *         待调整的日期时间对象, 可以是任意时区。
     * @return 调整为GMT时区的日期时间对象。
     * @throws NullPointerException
     *         如果参数{@code c}是{@code null}。
     */
    public static Calendar toGMT(final Calendar c) {
        notNull(c, "c");

        Calendar rc = Calendar.getInstance(TimeZone.getTimeZone(GMT_TIMEZONE_ID));
        rc.setLenient(false);
        rc.setTime(c.getTime());

        return rc;
    }

    /**
     * 将日期时间对象转化为时间戳对象。
     *
     * @param c
     *         待转化的日期时间对象。
     * @return 转化后的时间戳对象, 该对象和原对象表示同样的日期和时间。
     * @throws NullPointerException
     *         如果参数{@code c}是{@code null}。
     */
    public static Timestamp toTimestamp(final Calendar c) {
        notNull(c, "c");

        return new Timestamp(c.getTime().getTime());
    }

    /**
     * 获取指定年月的最大天数。
     *
     * <pre>
     * 示例：
     *
     * daysOfMonth(2000, 0) == 31;
     * daysOfMonth(2000, 1) == 29;
     * </pre>
     *
     * @param year
     *         指定的年份
     * @param month
     *         指定的月份
     * @return 指定年月的最大天数。
     * @throws ArrayIndexOutOfBoundsException
     *         如果指定的年月超出有效范围。
     */
    public static int daysOfMonth(
            final int year,
            final int month
    ) {
        final Calendar c = Calendar.getInstance();
        c.set(year, month, 1, 0, 0, 0);
        return c.getActualMaximum(Calendar.DAY_OF_MONTH);
    }

    /**
     * 获取指定日期的所在年月的最大天数。
     *
     * @param d
     *         指定的日期
     * @return 指定日期的所在年月的最大天数。
     * @throws NullPointerException
     *         如果{@code d == null}。
     */
    public static int daysOfMonth(final Date d) {
        if (d == null) {
            throw new NullPointerException();
        }

        final Calendar c = Calendar.getInstance();
        c.setTime(d);
        return c.getActualMaximum(Calendar.DATE);
    }

    /**
     * 复制日期时间对象, 用指定的参数替换指定的域, 如果指定的参数是{@code -1}, 那么对应的域不被替换。
     *
     * <p>
     * 毫秒部分是{@code 0}。
     * </p>
     *
     * @param c
     *         原日期时间对象, 如果此参数是{@code null}, 那么使用当前日期时间。
     * @param year
     *         需要替换的年份。
     * @param month
     *         需要替换的月份。
     * @param day
     *         需要替换的每月日期。
     * @param hour
     *         需要替换的小时。
     * @param minute
     *         需要替换的分钟。
     * @param second
     *         需要替换的秒。
     * @return 复制并且替换后的结果。
     * @see #copy(Calendar, int, int, int, int, int, int, int)
     */
    public static Calendar copy(
            final Calendar c,
            final int year,
            final int month,
            final int day,
            final int hour,
            final int minute,
            final int second
    ) {
        return copy(c, year, month, day, hour, minute, second, 0);
    }

    /**
     * 复制日期时间对象, 用指定的参数替换指定的域, 如果指定的参数是{@code -1}, 那么对应的域不被替换。
     *
     * @param c
     *         原日期时间对象, 如果此参数是{@code null}, 那么使用当前日期时间。
     * @param year
     *         需要替换的年份。
     * @param month
     *         需要替换的月份。
     * @param day
     *         需要替换的每月日期。
     * @param hour
     *         需要替换的小时。
     * @param minute
     *         需要替换的分钟。
     * @param second
     *         需要替换的秒。
     * @param mills
     *         需要替换的毫秒。
     * @return 复制并且替换后的结果。
     */
    public static Calendar copy(
            Calendar c,
            int year,
            int month,
            int day,
            int hour,
            int minute,
            int second,
            int mills
    ) {
        final Calendar cc = c == null ? Calendar.getInstance() : c;

        Calendar rc = Calendar.getInstance(cc.getTimeZone());
        rc.setLenient(false);

        if (month < 0) {
            month = cc.get(Calendar.MONTH);
        }
        //noinspection MagicConstant
        rc.set(year == -1 ? cc.get(Calendar.YEAR) : year,
               month,
               day == -1 ? cc.get(Calendar.DATE) : day,
               hour == -1 ? cc.get(Calendar.HOUR_OF_DAY) : hour,
               minute == -1 ? cc.get(Calendar.MINUTE) : minute,
               second == -1 ? cc.get(Calendar.SECOND) : second);
        rc.set(Calendar.MILLISECOND, mills);

        return rc;
    }

    /**
     * 计算两个GMT日期之间的差值。
     *
     * @param d1
     *         第一个日期。
     * @param d2
     *         第二个日期。
     * @return 两个日期之间的差值，以日为单位，如果参数{@code d1 > d2} ，那么返回正值，如果参数{@code d1 < d2}，那么返回负值，如果参数
     * {@code d1 == d2}，那么返回{@code 0}。
     */
    public static double diffDays(
            Date d1,
            Date d2
    ) {
        notNull(d1, "d1");
        notNull(d2, "d2");

        return (d1.getTime() - d2.getTime()) / ((double) MILLISECONDS_PER_DAY);
    }

    /**
     * 计算两个日期之间的差值，按照指定的时区忽略时分秒部分。
     *
     * @param d1
     *         第一个日期。
     * @param d2
     *         第二个日期。
     * @param timeZone
     *         指定的时区。
     * @return 两个日期之间的差值，以日为单位，如果参数{@code d1 > d2} ，那么返回正值，如果参数{@code d1 < d2}，那么返回负值，如果参数
     * {@code d1 == d2}，那么返回{@code 0}。
     */
    public static double diffDays(
            Date d1,
            Date d2,
            TimeZone timeZone
    ) {
        notNull(d1, "d1");
        notNull(d2, "d2");
        notNull(timeZone, "timeZone");

        final long oft1 = timeZone.getOffset(d1.getTime());
        final long oft2 = timeZone.getOffset(d2.getTime());

        return (d1.getTime() + oft1) / MILLISECONDS_PER_DAY - (d2.getTime() + oft2) / MILLISECONDS_PER_DAY;
    }

    /**
     * 计算两个本地日期之间的差值，忽略2个日期的时分秒部分。
     *
     * @param d1
     *         第一个日期。
     * @param d2
     *         第二个日期。
     * @return 两个日期之间的差值，以日为单位，如果参数{@code d1 > d2} ，那么返回正值，如果参数{@code d1 < d2}，那么返回负值，如果参数
     * {@code d1 == d2}，那么返回{@code 0}。
     */
    public static double diffDays2(
            Date d1,
            Date d2
    ) {
        return diffDays(d1, d2, TimeZone.getDefault());
    }

    /**
     * 计算两个本地日期之间的差值。
     *
     * @param d1
     *         第一个日期。
     * @param d2
     *         第二个日期。
     * @return 两个日期之间的差值，以月为单位，忽略日和时分秒部分，如果参数{@code d1 > d2} ，那么返回正值，如果参数{@code d1 < d2}，那么返回负值，如果参数
     * {@code d1 == d2}，那么返回{@code 0}。
     */
    public static int diffMonths2(
            final Date d1,
            final Date d2
    ) {
        notNull(d1, "d1");
        notNull(d2, "d2");

        Calendar cal = Calendar.getInstance();

        cal.setTime(d1);
        int year1 = cal.get(Calendar.YEAR);
        int month1 = cal.get(Calendar.MONTH);

        cal.setTime(d2);
        int year2 = cal.get(Calendar.YEAR);
        int month2 = cal.get(Calendar.MONTH);

        return (year1 - year2) * 12 + month1 - month2;
    }

    /**
     * 根据指定日期和持续天数计算出日期范围。
     *
     * <p>参数{@code date}会按照指定的时区被截取到该日的零点零分。作为第一个时间点。然后加上指定的天数，作为第二个时间点。然后以较早的日期作为起始时间点，较晚的作为结束时间点，创建日期范围。</p>
     *
     * @param date
     *         指定的日期。
     * @param days
     *         从指定日期开始的持续天数。
     * @param timeZone
     *         时区，如果此参数是{@code null}则使用系统默认时区。
     * @return 日期范围。
     * @throws IllegalArgumentException
     *         如果参数{@code date}是{@code null}。
     */
    public static DateRange daysRange(
            Date date,
            int days,
            TimeZone timeZone
    ) {
        notNull(date, "date");
        if (timeZone == null) {
            timeZone = TimeZone.getDefault();
        }

        Calendar c = Calendar.getInstance(timeZone);
        c.setTime(date);

        c = DateUtils.truncate(c, Calendar.DATE);
        final Date start = c.getTime();
        c.add(Calendar.DATE, days);
        final Date end = c.getTime();

        return days >= 0 ? new DateRange(start, end) : new DateRange(end, start);
    }
}
